<?php

class My_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract
{

    const EXCEPTION_NOT_ALLOWED = 'EXCEPTION_NOT_ALLOWED';

    const EXCEPTION_ROLE_NOT_FOUND = 'EXCEPTION_ROLE_NOT_FOUND';

    const EXCEPTION_OTHER = 'EXCEPTION_OTHER';

    const CACHE_ID = __CLASS__;

    protected $_acl;

    protected $_role;

    protected $_allRoles;

    protected $_resourcePrefix = '';

    protected $_resourceSeparator = ':';

    protected $_errorModule;

    protected $_errorController;

    protected $_errorAction;

    public function __construct($options = array())
    {
        $this->setOptions($options);
    }

    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        $this->_init();
        
        try {
            if (!$this->isRequestAllowed($request)) {
                /**
                 * @see Zend_Controller_Exception
                 */
                require_once 'Zend/Controller/Exception.php';
                throw new Zend_Controller_Exception('Role "' . $this->getRole()->getRoleId() . '" does not have access to requested resource');
            }
        } catch (Zend_Exception $e) {
            if (Zend_Controller_Front::getInstance()->throwExceptions()) {
                throw $e;
            }
            
            $errorHandler = new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS);
            
            switch (get_class($e)) {
                case 'Zend_Acl_Exception':
                    $errorHandler->type = self::EXCEPTION_ROLE_NOT_FOUND;
                    break;
                case 'Zend_Controller_Exception':
                    $errorHandler->type = self::EXCEPTION_NOT_ALLOWED;
                    break;
                default:
                    $errorHandler->type = self::EXCEPTION_OTHER;
            }
            
            $errorHandler->exception = $e;
            $errorHandler->request = clone $request;
            
            $request->setParam('error_handler', $errorHandler)
                    ->setModuleName($this->getErrorHandlerModule())
                    ->setControllerName($this->getErrorHandlerController())
                    ->setActionName($this->getErrorHandlerAction())
                    ->setDispatched(false);
        }
    }

    protected function _init()
    {
        $_start = microtime(true);
        
        $logger = Zend_Registry::get('logger');
        $cache = Zend_Registry::get('cache');
        
        if (!($serializedAcl = $cache->load(self::CACHE_ID))) {
            
            $this->_acl = new Zend_Acl();
            
            $modules = Doctrine_Query::create()->select('m.name, r.name')->from('AclModule m')->leftJoin('m.Resources r')->fetchArray();
            
            foreach ($modules as $module) {
                if (!$this->_acl->has($module['name'])) {
                    $this->_acl->add(new Zend_Acl_Resource($module['name']));
                    $logger->info('Added module: ' . $module['name']);
                }
                
                foreach ($module['Resources'] as $resource) {
                    if (!$this->_acl->has($module['name'] . ':' . $resource['name'])) {
                        $this->_acl->add(new Zend_Acl_Resource($module['name'] . ':' . $resource['name']), $module['name']);
                        
                        $logger->info('Added resourceId: ' . $module['name'] . ':' . $resource['name'] . ' for module: ' . $module['name']);
                    }
                }
            }
            
            $roles = Doctrine_Query::create()->select('r.name, r.issuper, pa.name, m.name, re.name, rm.name, p.name, pr.name, prm.name')->from('AclRole r')->leftJoin('r.Parents pa')->leftJoin('r.Modules m')->leftJoin('r.Resources re')->leftJoin('re.Module as rm')->leftJoin('r.Privileges p')->leftJoin('p.Resource pr')->leftJoin('pr.Module prm')->fetchArray();
            
            $this->_allRoles = array();
            
            foreach ($roles as $role) {
                $this->_allRoles[$role['name']] = $role;
            }
            
            unset($roles);
            
            foreach ($this->_allRoles as $roleName => $roleData) {
                $this->_addRole($roleName);
                
                //$this->_acl->deny($roleName);
                

                if ($roleData['issuper']) {
                    $this->_acl->allow($roleName);
                    $logger->info("Allowed $roleName as super");
                }
                else {
                    $allowedModules = array();
                    
                    foreach ($roleData['Modules'] as $module) {
                        $allowedModules[] = $module['name'];
                        $this->_acl->allow($roleName, $module['name']);
                        $logger->info("Allowed $roleName for module: " . $module['name']);
                    }
                    
                    $allowedResources = array();
                    
                    foreach ($roleData['Resources'] as $resource) {
                        $resourceId = $resource['Module']['name'] . ':' . $resource['name'];
                        $allowedResources[] = $resourceId;
                        
                        if (!in_array($resource['Module']['name'], $allowedModules)) {
                            $this->_acl->allow($roleName, $resourceId);
                            $logger->info("Allowed $roleName for resourceId: " . $resourceId);
                        }
                    }
                    
                    foreach ($roleData['Privileges'] as $privilege) {
                        if (!in_array($privilege['Resource']['Module']['name'], $allowedModules)) {
                            $resourceId = $privilege['Resource']['Module']['name'] . ':' . $privilege['Resource']['name'];
                            if (!in_array($resourceId, $allowedResources)) {
                                $this->_acl->allow($roleName, $resourceId, $privilege['name']);
                                $logger->info("Allowed $roleName for resourceId: " . $resourceId . " with privilege: " . $privilege['name']);
                            }
                        }
                    }
                }
            }
            
            $cache->save(serialize($this->_acl), self::CACHE_ID);
        }
        else {
            $this->_acl = unserialize($serializedAcl);
        }
        
        $logger->info('Eaten up: ' . (microtime(true) - $_start));
    }

    private function _addRole($name)
    {
        $logger = Zend_Registry::get('logger');
        
        if (isset($this->_allRoles[$name])) {
            $parentNames = array();
            
            foreach ($this->_allRoles[$name]['Parents'] as $parent) {
                $parentNames[] = $parent['name'];
                
                if (!$this->_acl->hasRole($parent['name']))
                    $this->_addRole($parent['name']);
            }
            
            if (!$this->_acl->hasRole($name)) {
                $this->_acl->addRole(new Zend_Acl_Role($name), $parentNames);
                $logger->info('Added role: ' . $name . ' with parents: ' . implode(', ', $parentNames));
            }
        }
    }

    public function setOptions(array $options = array())
    {
        $validOptions = array('resourcePrefix', 'resourceSeparator', 'errorHandlerModule', 'errorHandlerController', 'errorHandlerAction');
        
        foreach ($validOptions as $option) {
            if (array_key_exists($option, $options)) {
                $this->{'set' . $option}($options[$option]);
            }
        }
        
        return $this;
    }

    /**
     * Returns the ACL.
     *
     * @return  Zend_Acl    The ACL.
     */
    public function getAcl()
    {
        if (null === $this->_acl) {
            $this->_acl = new Zend_Acl();
        }
        
        return $this->_acl;
    }

    /**
     * Returns the ACL role.
     *
     * @return  Zend_Acl_Role_Interface An ACL role.
     */
    public function getRole()
    {
        if (empty($this->_role)) {
            $identity = Zend_Auth::getInstance()->getIdentity();
            $this->_role = new Zend_Acl_Role($identity['Role']['name']);
        }
        
        return $this->_role;
    }

    /**
     * Sets the resource prefix.
     *
     * @param   string  $prefix A resource ID prefix.
     *
     * @return  My_Controller_Plugin_Acl
     */
    public function setResourcePrefix($prefix)
    {
        $this->_resourcePrefix = (string) $prefix;
        
        return $this;
    }

    /**
     * Returns the resource prefix.
     *
     * @return  string  The resource ID prefix.
     */
    public function getResourcePrefix()
    {
        return $this->_resourcePrefix;
    }

    /**
     * Sets the resource separator.
     *
     * @param   string  $separator  A resource ID separator.
     *
     * @return  My_Controller_Plugin_Acl
     */
    public function setResourceSeparator($separator)
    {
        $this->_resourceSeparator = (string) $separator;
        
        return $this;
    }

    /**
     * Returns the resource separator.
     *
     * @return  string  A resource ID separator.
     */
    public function getResourceSeparator()
    {
        return $this->_resourceSeparator;
    }

    /**
     * Sets the error handler module.
     *
     * @param   string  $module The module to forward to when an error occurs.
     *
     * @return  My_Controller_Plugin_Acl
     */
    public function setErrorHandlerModule($module)
    {
        $this->_errorModule = (string) $module;
        
        return $this;
    }

    /**
     * Returns the error handler module, defaulting to the Zend_Controller_Plugin_ErrorHandler plugin
     * or the dispatcher's default module.
     *
     * @return  string  The module to forward to when an error occurs.
     */
    public function getErrorHandlerModule()
    {
        if (null === $this->_errorModule) {
            $frontController = Zend_Controller_Front::getInstance();
            
            if (($errorHandler = $frontController->getPlugin('My_Controller_Plugin_ErrorHandler'))) {
                $this->_errorModule = $errorHandler->getErrorHandlerModule();
            }
            else {
                $this->_errorModule = $frontController->getDispatcher()->getDefaultModule();
            }
        }
        
        return $this->_errorModule;
    }

    /**
     * Sets the error handler controller.
     *
     * @param   string  $controller The controller to forward to when an error occurs.
     *
     * @return  My_Controller_Plugin_Acl
     */
    public function setErrorHandlerController($controller)
    {
        $this->_errorController = (string) $controller;
        
        return $this;
    }

    /**
     * Returns the error handler controller, defaulting to the Zend_Controller_Plugin_ErrorHandler plugin..
     *
     * @return  string  The controller to forward to when an error occurs.
     */
    public function getErrorHandlerController()
    {
        if (null === $this->_errorController) {
            $frontController = Zend_Controller_Front::getInstance();
            
            if (($errorHandler = $frontController->getPlugin('My_Controller_Plugin_ErrorHandler'))) {
                $this->_errorController = $errorHandler->getErrorHandlerController();
            }
            else {
                $this->_errorController = 'error';
            }
        }
        
        return $this->_errorController;
    }

    /**
     * Sets the error handler action.
     *
     * @param   string  $action The action to forward to when an error occurs.
     *
     * @return  My_Controller_Plugin_Acl
     */
    public function setErrorHandlerAction($action)
    {
        $this->_errorAction = (string) $action;
        
        return $this;
    }

    /**
     * Returns the error handler action, defaulting to the Zend_Controller_Plugin_ErrorHandler plugin..
     *
     * @return  string  The action to forward to when an error occurs.
     */
    public function getErrorHandlerAction()
    {
        if (null === $this->_errorAction) {
            $frontController = Zend_Controller_Front::getInstance();
            
            if ($errorHandler = $frontController->getPlugin('Zend_Controller_Plugin_ErrorHandler')) {
                $this->_errorAction = $errorHandler->getErrorHandlerAction();
            }
            else {
                $this->_errorAction = 'error';
            }
        }
        
        return $this->_errorAction;
    }

    /**
     * Returns a resource ID for the given module/controller/action.
     *
     * @param   string  $module     A module.
     * @param   string  $controller A controller.
     * @param   string  $action     An action.
     *
     * @return  string  The ACL resource ID for the given module/controller/action.
     */
    public function getResourceId($module, $controller = false, $action = false)
    {
        $frontController = Zend_Controller_Front::getInstance();
        
        if (empty($module)) {
            $module = $frontController->getDefaultModule();
        }
        
        $resourceId = $this->getResourcePrefix() . ((string) $module);
        
        if (false !== $controller) {
            if (empty($controller)) {
                $controller = $frontController->getDefaultControllerName();
            }
            
            $resourceId .= $this->getResourceSeparator() . ((string) $controller);
            
            if (false !== $action) {
                if (empty($action)) {
                    $action = $frontController->getDefaultAction();
                }
                
                $resourceId .= $this->getResourceSeparator() . ((string) $action);
            }
        }
        
        return $resourceId;
    }

    public function isRequestAllowed(Zend_Controller_Request_Abstract $request)
    {
        if (!Zend_Controller_Front::getInstance()->getDispatcher()->isDispatchable($request)) {
            return true;
        }
        
        $module = $request->getModuleName();
        $controller = $request->getControllerName();
        $action = $request->getActionName();
        
        return $this->isAllowed($module, $controller, $action);
    }

    public function isAllowed($module, $controller, $action)
    {
        $currentRole = $this->getRole();
        
        if (($this->getErrorHandlerModule() == $module) && ($this->getErrorHandlerController() == $controller) && ($this->getErrorHandlerAction() == $action)) {
            return true;
        }
        
        $acl = $this->getAcl();
        $controllerResourceId = $this->getResourceId($module, $controller);
        
        if (!$acl->has($module)) {
            $acl->add(new Zend_Acl_Resource($module));
        }
        
        if (!$acl->has($controllerResourceId)) {
            $acl->add(new Zend_Acl_Resource($controllerResourceId), $module);
        }
        
        return $this->getAcl()->isAllowed($currentRole, $controllerResourceId, $action);
    }

}